// Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information.
namespace FSharp.Compiler.IO

open System
open System.IO
#if !FABLE_COMPILER
open System.IO.MemoryMappedFiles
open System.Buffers
open System.Reflection
open System.Threading
open System.Runtime.InteropServices
open FSharp.NativeInterop
#endif
open Internal.Utilities.Library

open System.Text

exception IllegalFileNameChar of string * char

#nowarn "9"

module internal Bytes =

    let b0 n = (n &&& 0xFF)

    let b1 n = ((n >>> 8) &&& 0xFF)

    let b2 n = ((n >>> 16) &&& 0xFF)

    let b3 n = ((n >>> 24) &&& 0xFF)

    let dWw1 n = int32 ((n >>> 32) &&& 0xFFFFFFFFL)

    let dWw0 n = int32 (n &&& 0xFFFFFFFFL)

    let get (b: byte[]) n = int32 (Array.get b n)

    let zeroCreate n : byte[] = Array.zeroCreate n

    let blit (a: byte[]) b c d e = Array.blit a b c d e

    let ofInt32Array (arr: int[]) =
        Array.init arr.Length (fun i -> byte arr[i])

    let stringAsUtf8NullTerminated (s: string) =
        Array.append (Encoding.UTF8.GetBytes s) (ofInt32Array [| 0x0 |])

    let stringAsUnicodeNullTerminated (s: string) =
        Array.append (Encoding.Unicode.GetBytes s) (ofInt32Array [| 0x0; 0x0 |])

[<Experimental("This FCS API/Type is experimental and subject to change.")>]
[<AbstractClass>]
type ByteMemory() =
    abstract Item: int -> byte with get, set
    abstract Length: int
    abstract ReadAllBytes: unit -> byte[]
    abstract ReadBytes: pos: int * count: int -> byte[]
    abstract ReadInt32: pos: int -> int
    abstract ReadUInt16: pos: int -> uint16
    abstract ReadUtf8String: pos: int * count: int -> string
    abstract Slice: pos: int * count: int -> ByteMemory
#if !FABLE_COMPILER
    abstract CopyTo: Stream -> unit
#endif
    abstract Copy: srcOffset: int * dest: byte[] * destOffset: int * count: int -> unit
    abstract ToArray: unit -> byte[]
#if !FABLE_COMPILER
    abstract AsStream: unit -> Stream
    abstract AsReadOnlyStream: unit -> Stream
#endif

[<Experimental("This FCS API/Type is experimental and subject to change.")>]
[<Sealed>]
type ByteArrayMemory(bytes: byte[], offset, length) =
    inherit ByteMemory()

    let checkCount count =
        if count < 0 then
            raise (ArgumentOutOfRangeException("count", "Count is less than zero."))

    do
        if length < 0 || length > bytes.Length then
            raise (ArgumentOutOfRangeException("length"))

        if offset < 0 || (offset + length) > bytes.Length then
            raise (ArgumentOutOfRangeException("offset"))

    override _.Item
        with get i = bytes[offset + i]
        and set i v = bytes[offset + i] <- v

    override _.Length = length

    override _.ReadAllBytes() = bytes

    override _.ReadBytes(pos, count) =
        checkCount count

        if count > 0 then
            Array.sub bytes (offset + pos) count
        else
            Array.empty

    override _.ReadInt32 pos =
        let finalOffset = offset + pos

        (uint32 bytes[finalOffset])
        ||| ((uint32 bytes[finalOffset + 1]) <<< 8)
        ||| ((uint32 bytes[finalOffset + 2]) <<< 16)
        ||| ((uint32 bytes[finalOffset + 3]) <<< 24)
        |> int

    override _.ReadUInt16 pos =
        let finalOffset = offset + pos
        (uint16 bytes[finalOffset]) ||| ((uint16 bytes[finalOffset + 1]) <<< 8)

    override _.ReadUtf8String(pos, count) =
        checkCount count

        if count > 0 then
            Encoding.UTF8.GetString(bytes, offset + pos, count)
        else
            String.Empty

    override _.Slice(pos, count) =
        checkCount count

        if count > 0 then
            ByteArrayMemory(bytes, offset + pos, count) :> ByteMemory
        else
            ByteArrayMemory(Array.empty, 0, 0) :> ByteMemory

#if !FABLE_COMPILER
    override _.CopyTo stream =
        if length > 0 then
            stream.Write(bytes, offset, length)
#endif

    override _.Copy(srcOffset, dest, destOffset, count) =
        checkCount count

        if count > 0 then
            Array.blit bytes (offset + srcOffset) dest destOffset count

    override _.ToArray() =
        if length > 0 then
            Array.sub bytes offset length
        else
            Array.empty

#if !FABLE_COMPILER

    override _.AsStream() =
        if length > 0 then
            new MemoryStream(bytes, offset, length) :> Stream
        else
            new MemoryStream([||], 0, 0, false) :> Stream

    override _.AsReadOnlyStream() =
        if length > 0 then
            new MemoryStream(bytes, offset, length, false) :> Stream
        else
            new MemoryStream([||], 0, 0, false) :> Stream

[<Experimental("This FCS API/Type is experimental and subject to change.")>]
[<Sealed>]
type SafeUnmanagedMemoryStream =
    inherit UnmanagedMemoryStream

    val mutable private holder: objnull
    val mutable private isDisposed: bool

    new(addr, length, holder) =
        {
            inherit UnmanagedMemoryStream(addr, length)
            holder = holder
            isDisposed = false
        }

    new(addr: nativeptr<byte>, length: int64, capacity: int64, access: FileAccess, holder) =
        {
            inherit UnmanagedMemoryStream(addr, length, capacity, access)
            holder = holder
            isDisposed = false
        }

    override x.Dispose disposing =
        base.Dispose disposing
        x.holder <- null // Null out so it can be collected.

type internal MemoryMappedStream(mmf: MemoryMappedFile, length: int64) =
    inherit Stream()

    let viewStream = mmf.CreateViewStream(0L, length, MemoryMappedFileAccess.Read)

    member _.ViewStream = viewStream

    override x.CanRead = viewStream.CanRead
    override x.CanWrite = viewStream.CanWrite
    override x.CanSeek = viewStream.CanSeek

    override x.Position
        with get () = viewStream.Position
        and set v = viewStream.Position <- v

    override x.Length = viewStream.Length
    override x.Flush() = viewStream.Flush()
    override x.Seek(offset, origin) = viewStream.Seek(offset, origin)
    override x.SetLength(value) = viewStream.SetLength(value)
    override x.Write(buffer, offset, count) = viewStream.Write(buffer, offset, count)
    override x.Read(buffer, offset, count) = viewStream.Read(buffer, offset, count)

    override x.Finalize() = x.Dispose()

    interface IDisposable with
        override x.Dispose() =
            GC.SuppressFinalize x
            mmf.Dispose()
            viewStream.Dispose()

[<Experimental("This FCS API/Type is experimental and subject to change.")>]
type RawByteMemory(addr: nativeptr<byte>, length: int, holder: obj) =
    inherit ByteMemory()

    let check i =
        if i < 0 || i >= length then
            raise (ArgumentOutOfRangeException(nameof i))

    let checkCount count =
        if count < 0 then
            raise (ArgumentOutOfRangeException(nameof count, "Count is less than zero."))

    do
        if length < 0 then
            raise (ArgumentOutOfRangeException(nameof length))

    override _.Item
        with get i =
            check i
            NativePtr.add addr i |> NativePtr.read
        and set i v =
            check i
            NativePtr.set addr i v

    override _.Length = length

    override this.ReadAllBytes() = this.ReadBytes(0, length)

    override _.ReadBytes(pos, count) =
        checkCount count

        if count > 0 then
            check pos
            check (pos + count - 1)
            let res = Bytes.zeroCreate count
            Marshal.Copy(NativePtr.toNativeInt addr + nativeint pos, res, 0, count)
            res
        else
            Array.empty

    override _.ReadInt32 pos =
        check pos
        check (pos + 3)
        let finalAddr = NativePtr.toNativeInt addr + nativeint pos

        uint32 (Marshal.ReadByte(finalAddr, 0))
        ||| (uint32 (Marshal.ReadByte(finalAddr, 1)) <<< 8)
        ||| (uint32 (Marshal.ReadByte(finalAddr, 2)) <<< 16)
        ||| (uint32 (Marshal.ReadByte(finalAddr, 3)) <<< 24)
        |> int

    override _.ReadUInt16 pos =
        check pos
        check (pos + 1)
        let finalAddr = NativePtr.toNativeInt addr + nativeint pos

        uint16 (Marshal.ReadByte(finalAddr, 0))
        ||| (uint16 (Marshal.ReadByte(finalAddr, 1)) <<< 8)

    override _.ReadUtf8String(pos, count) =
        checkCount count

        if count > 0 then
            check pos
            check (pos + count - 1)
            Encoding.UTF8.GetString(NativePtr.add addr pos, count)
        else
            String.Empty

    override _.Slice(pos, count) =
        checkCount count

        if count > 0 then
            check pos
            check (pos + count - 1)
            RawByteMemory(NativePtr.add addr pos, count, holder) :> ByteMemory
        else
            ByteArrayMemory(Array.empty, 0, 0) :> ByteMemory

    override x.CopyTo stream =
        if length > 0 then
            use stream2 = x.AsStream()
            stream2.CopyTo stream

    override _.Copy(srcOffset, dest, destOffset, count) =
        checkCount count

        if count > 0 then
            check srcOffset
            Marshal.Copy(NativePtr.toNativeInt addr + nativeint srcOffset, dest, destOffset, count)

    override _.ToArray() =
        if length > 0 then
            let res = Array.zeroCreate<byte> length
            Marshal.Copy(NativePtr.toNativeInt addr, res, 0, res.Length)
            res
        else
            Array.empty

    override _.AsStream() =
        if length > 0 then
            new SafeUnmanagedMemoryStream(addr, int64 length, holder) :> Stream
        else
            new MemoryStream([||], 0, 0, false) :> Stream

    override _.AsReadOnlyStream() =
        if length > 0 then
            new SafeUnmanagedMemoryStream(addr, int64 length, int64 length, FileAccess.Read, holder) :> Stream
        else
            new MemoryStream([||], 0, 0, false) :> Stream

#endif //!FABLE_COMPILER

[<Struct; NoEquality; NoComparison>]
type ReadOnlyByteMemory(bytes: ByteMemory) =

    member _.Item
        with get i = bytes[i]

    member _.Length = bytes.Length

    member _.ReadAllBytes() = bytes.ReadAllBytes()

    member _.ReadBytes(pos, count) = bytes.ReadBytes(pos, count)

    member _.ReadInt32 pos = bytes.ReadInt32 pos

    member _.ReadUInt16 pos = bytes.ReadUInt16 pos

    member _.ReadUtf8String(pos, count) = bytes.ReadUtf8String(pos, count)

    member _.Slice(pos, count) =
        bytes.Slice(pos, count) |> ReadOnlyByteMemory

#if !FABLE_COMPILER
    member _.CopyTo stream = bytes.CopyTo stream
#endif

    member _.Copy(srcOffset, dest, destOffset, count) =
        bytes.Copy(srcOffset, dest, destOffset, count)

    member _.ToArray() = bytes.ToArray()

#if !FABLE_COMPILER
    member _.AsStream() = bytes.AsReadOnlyStream()

    member _.Underlying = bytes
#endif

#if !FABLE_COMPILER

[<AutoOpen>]
module MemoryMappedFileExtensions =

    let private trymmf length copyTo =
        let length = int64 length

        if length = 0L then
            None
        else
            // Try to create a memory mapped file and copy the contents of the given bytes to it.
            // If this fails, then we clean up and return None.
            try
                let mmf =
                    MemoryMappedFile.CreateNew(
                        null,
                        length,
                        MemoryMappedFileAccess.ReadWrite,
                        MemoryMappedFileOptions.None,
                        HandleInheritability.None
                    )

                try
                    use stream = mmf.CreateViewStream(0L, length, MemoryMappedFileAccess.ReadWrite)
                    copyTo stream
                    Some mmf
                with _ ->
                    mmf.Dispose()
                    None
            with _ ->
                None

    type MemoryMappedFile with

        static member TryFromByteMemory(bytes: ReadOnlyByteMemory) =
            trymmf (int64 bytes.Length) bytes.CopyTo

        static member TryFromMemory(bytes: ReadOnlyMemory<byte>) =
            let length = int64 bytes.Length

            trymmf length (fun stream ->
                let span = Span<byte>(stream.PositionPointer |> NativePtr.toVoidPtr, int length)
                bytes.Span.CopyTo(span)
                stream.Position <- stream.Position + length)

#endif //!FABLE_COMPILER

[<RequireQualifiedAccess>]
module internal FileSystemUtils =
    let checkPathForIllegalChars =
        let chars = System.Collections.Generic.HashSet<_>(Path.GetInvalidPathChars())

        (fun (path: string) ->
            for c in path do
                if chars.Contains c then
                    raise (IllegalFileNameChar(path, c)))

    let checkSuffix (path: string) (suffix: string) = path.EndsWithOrdinalIgnoreCase(suffix)

    let hasExtensionWithValidate (validate: bool) (s: string) =
        if validate then
            (checkPathForIllegalChars s)

        let sLen = s.Length

        (sLen >= 1 && s[sLen - 1] = '.' && s <> ".." && s <> ".")
        || Path.HasExtension(s)

    let hasExtension (path: string) = hasExtensionWithValidate true path

    let chopExtension (path: string) =
        checkPathForIllegalChars path

        if path = "." then
            ""
        else // for OCaml compatibility
            if not (hasExtensionWithValidate false path) then
                raise (ArgumentException("chopExtension")) // message has to be precisely this, for OCaml compatibility, and no argument name can be set

            Path.Combine(!!Path.GetDirectoryName(path), !!Path.GetFileNameWithoutExtension(path))

    let fileNameOfPath path =
        checkPathForIllegalChars path
        !!Path.GetFileName(path)

    let fileNameWithoutExtensionWithValidate (validate: bool) path =
        if validate then
            checkPathForIllegalChars path

        !!Path.GetFileNameWithoutExtension(path)

    let fileNameWithoutExtension path =
        !! fileNameWithoutExtensionWithValidate true path

    let trimQuotes (path: string) = path.Trim([| ' '; '\"' |])

    let isDll fileName = checkSuffix fileName ".dll"

#if FABLE_COMPILER

[<AbstractClass; Sealed>]
type FileSystem =

    static member GetFullPathShim (fileName: string) =
        fileName // not getting a full path, unless it already is

    static member IsPathRootedShim (path: string) =
        path.StartsWith("/") || path.StartsWith("\\") || path.IndexOf(':') = 1

    static member NormalizePathShim (path: string) =
        let path =
            if FileSystem.IsPathRootedShim path
            then FileSystem.GetFullPathShim path
            else path
        path.Replace('\\', '/')

    static member GetFullFilePathInDirectoryShim (dir: string) (fileName: string) =
        let path =
            if FileSystem.IsPathRootedShim(fileName)
            then fileName
            else Path.Combine(dir, fileName)
        FileSystem.GetFullPathShim(path)

    static member IsInvalidPathShim(path: string) =
        let isInvalidPath(p: string) =
            String.IsNullOrEmpty p || p.IndexOfAny(Path.GetInvalidPathChars()) <> -1
        let isInvalidFilename(p: string) =
            String.IsNullOrEmpty p || p.IndexOfAny(Path.GetInvalidFileNameChars()) <> -1
        let isInvalidDirectory(p: string) =
            String.IsNullOrEmpty p || p.IndexOfAny(Path.GetInvalidPathChars()) <> -1
        isInvalidPath path ||
        let directory = Path.GetDirectoryName path
        let filename = Path.GetFileName path
        isInvalidDirectory directory || isInvalidFilename filename

    static member GetTempPathShim() = "."

    static member GetDirectoryNameShim(path: string) =
        Path.GetDirectoryName(path)

#else //!FABLE_COMPILER

[<Experimental("This FCS API/Type is experimental and subject to change.")>]
type IAssemblyLoader =

    abstract AssemblyLoadFrom: fileName: string -> Assembly

    abstract AssemblyLoad: assemblyName: AssemblyName -> Assembly

[<Experimental("This FCS API/Type is experimental and subject to change.")>]
type DefaultAssemblyLoader() =

    interface IAssemblyLoader with

        member _.AssemblyLoadFrom(fileName: string) = Assembly.UnsafeLoadFrom fileName

        member _.AssemblyLoad(assemblyName: AssemblyName) = Assembly.Load assemblyName

[<Experimental("This FCS API/Type is experimental and subject to change.")>]
type IFileSystem =
    // note: do not add members if you can put generic implementation under StreamExtensions below.

    abstract AssemblyLoader: IAssemblyLoader

    abstract OpenFileForReadShim: filePath: string * ?useMemoryMappedFile: bool * ?shouldShadowCopy: bool -> Stream

    abstract OpenFileForWriteShim: filePath: string * ?fileMode: FileMode * ?fileAccess: FileAccess * ?fileShare: FileShare -> Stream

    abstract GetFullPathShim: fileName: string -> string

    abstract GetFullFilePathInDirectoryShim: dir: string -> fileName: string -> string

    abstract IsPathRootedShim: path: string -> bool

    abstract NormalizePathShim: path: string -> string

    abstract IsInvalidPathShim: path: string -> bool

    abstract GetTempPathShim: unit -> string

    abstract GetDirectoryNameShim: path: string -> string

    abstract GetLastWriteTimeShim: fileName: string -> DateTime

    abstract GetCreationTimeShim: path: string -> DateTime

    abstract CopyShim: src: string * dest: string * overwrite: bool -> unit

    abstract FileExistsShim: fileName: string -> bool

    abstract FileDeleteShim: fileName: string -> unit

    abstract DirectoryCreateShim: path: string -> string

    abstract DirectoryExistsShim: path: string -> bool

    abstract DirectoryDeleteShim: path: string -> unit

    abstract EnumerateFilesShim: path: string * pattern: string -> string seq

    abstract EnumerateDirectoriesShim: path: string -> string seq

    abstract IsStableFileHeuristic: fileName: string -> bool

    abstract ChangeExtensionShim: path: string * extension: string -> string

// note: do not add members if you can put generic implementation under StreamExtensions below.

[<Experimental("This FCS API/Type is experimental and subject to change.")>]
type DefaultFileSystem() as this =
    abstract AssemblyLoader: IAssemblyLoader
    default _.AssemblyLoader = DefaultAssemblyLoader() :> IAssemblyLoader

    abstract OpenFileForReadShim: filePath: string * ?useMemoryMappedFile: bool * ?shouldShadowCopy: bool -> Stream

    default _.OpenFileForReadShim(filePath: string, ?useMemoryMappedFile: bool, ?shouldShadowCopy: bool) : Stream =
        let fileMode = FileMode.Open
        let fileAccess = FileAccess.Read
        let fileShare = FileShare.Delete ||| FileShare.ReadWrite
        let shouldShadowCopy = defaultArg shouldShadowCopy false
        let useMemoryMappedFile = defaultArg useMemoryMappedFile false
        let fileStream = new FileStream(filePath, fileMode, fileAccess, fileShare)
        let length = fileStream.Length

        // We want to use mmapped files only when:
        //   -  Opening large binary files (no need to use for source or resource files really)

        if not useMemoryMappedFile then
            fileStream :> Stream
        else
            let mmf =
                if shouldShadowCopy then
                    let mmf =
                        MemoryMappedFile.CreateNew(
                            null,
                            length,
                            MemoryMappedFileAccess.ReadWrite,
                            MemoryMappedFileOptions.None,
                            HandleInheritability.None
                        )

                    use stream = mmf.CreateViewStream(0L, length, MemoryMappedFileAccess.ReadWrite)
                    fileStream.CopyTo(stream)
                    fileStream.Dispose()
                    mmf
                else
                    MemoryMappedFile.CreateFromFile(
                        fileStream,
                        null,
                        length,
                        MemoryMappedFileAccess.Read,
                        HandleInheritability.None,
                        leaveOpen = false
                    )

            let stream = new MemoryMappedStream(mmf, length)

            if not stream.CanRead then
                invalidOp "Cannot read file"

            stream :> Stream

    abstract OpenFileForWriteShim: filePath: string * ?fileMode: FileMode * ?fileAccess: FileAccess * ?fileShare: FileShare -> Stream

    default _.OpenFileForWriteShim(filePath: string, ?fileMode: FileMode, ?fileAccess: FileAccess, ?fileShare: FileShare) : Stream =
        let fileMode = defaultArg fileMode FileMode.OpenOrCreate
        let fileAccess = defaultArg fileAccess FileAccess.ReadWrite
        let fileShare = defaultArg fileShare FileShare.Delete ||| FileShare.ReadWrite

        new FileStream(filePath, fileMode, fileAccess, fileShare) :> Stream

    abstract GetFullPathShim: fileName: string -> string
    default _.GetFullPathShim(fileName: string) = Path.GetFullPath fileName

    abstract GetFullFilePathInDirectoryShim: dir: string -> fileName: string -> string

    default this.GetFullFilePathInDirectoryShim (dir: string) (fileName: string) =
        let p =
            if (this :> IFileSystem).IsPathRootedShim(fileName) then
                fileName
            else
                Path.Combine(dir, fileName)

        try
            (this :> IFileSystem).GetFullPathShim(p)
        with
        | :? ArgumentException
        | :? ArgumentNullException
        | :? NotSupportedException
        | :? PathTooLongException
        | :? System.Security.SecurityException -> p

    abstract IsPathRootedShim: path: string -> bool
    default _.IsPathRootedShim(path: string) = Path.IsPathRooted path

    abstract NormalizePathShim: path: string -> string

    default _.NormalizePathShim(path: string) =
        try
            let ifs = this :> IFileSystem

            if ifs.IsPathRootedShim path then
                ifs.GetFullPathShim path
            else
                path
        with _ ->
            path

    abstract IsInvalidPathShim: path: string -> bool

    default _.IsInvalidPathShim(path: string) =
        let isInvalidPath (p: string MaybeNull) =
            if String.IsNullOrEmpty(p) then
                true
            else
                p.IndexOfAny(Path.GetInvalidPathChars()) <> -1

        let isInvalidFilename (p: string MaybeNull) =
            if String.IsNullOrEmpty(p) then
                true
            else
                p.IndexOfAny(Path.GetInvalidFileNameChars()) <> -1

        let isInvalidDirectory (d: string MaybeNull) =
            match d with
            | Null -> true
            | NonNull d -> d.IndexOfAny(Path.GetInvalidPathChars()) <> -1

        isInvalidPath path
        || let directory = Path.GetDirectoryName path in
           let fileName = Path.GetFileName path in
           isInvalidDirectory directory || isInvalidFilename fileName

    abstract GetTempPathShim: unit -> string
    default _.GetTempPathShim() = Path.GetTempPath()

    abstract GetDirectoryNameShim: path: string -> string

    default _.GetDirectoryNameShim(path: string) =
        FileSystemUtils.checkPathForIllegalChars path

        if String.IsNullOrEmpty(path) then
            "."
        else
            match Path.GetDirectoryName(path) with
            | null ->
                if (this :> IFileSystem).IsPathRootedShim(path) then
                    path
                else
                    "."
            | res -> if String.IsNullOrEmpty(res) then "." else res

    abstract GetLastWriteTimeShim: fileName: string -> DateTime
    default _.GetLastWriteTimeShim(fileName: string) = File.GetLastWriteTimeUtc fileName

    abstract GetCreationTimeShim: path: string -> DateTime
    default _.GetCreationTimeShim(path: string) = File.GetCreationTimeUtc path

    abstract CopyShim: src: string * dest: string * overwrite: bool -> unit
    default _.CopyShim(src: string, dest: string, overwrite: bool) = File.Copy(src, dest, overwrite)

    abstract FileExistsShim: fileName: string -> bool
    default _.FileExistsShim(fileName: string) = File.Exists fileName

    abstract FileDeleteShim: fileName: string -> unit
    default _.FileDeleteShim(fileName: string) = File.Delete fileName

    abstract DirectoryCreateShim: path: string -> string

    default _.DirectoryCreateShim(path: string) =
        let dir = Directory.CreateDirectory path
        dir.FullName

    abstract DirectoryExistsShim: path: string -> bool
    default _.DirectoryExistsShim(path: string) = Directory.Exists path

    abstract DirectoryDeleteShim: path: string -> unit
    default _.DirectoryDeleteShim(path: string) = Directory.Delete path

    abstract EnumerateFilesShim: path: string * pattern: string -> string seq
    default _.EnumerateFilesShim(path: string, pattern: string) = Directory.EnumerateFiles(path, pattern)

    abstract EnumerateDirectoriesShim: path: string -> string seq
    default _.EnumerateDirectoriesShim(path: string) = Directory.EnumerateDirectories(path)

    abstract IsStableFileHeuristic: fileName: string -> bool

    default _.IsStableFileHeuristic(fileName: string) =
        let directory = Path.GetDirectoryName fileName

        match directory with
        | Null -> false
        | NonNull directory ->
            directory.Contains("Reference Assemblies/")
            || directory.Contains("Reference Assemblies\\")
            || directory.Contains("packages/")
            || directory.Contains("packages\\")
            || directory.Contains("lib/mono/")

    abstract ChangeExtensionShim: path: string * extension: string -> string

    default _.ChangeExtensionShim(path: string, extension: string) : string = !!Path.ChangeExtension(path, extension)

    interface IFileSystem with
        member _.AssemblyLoader = this.AssemblyLoader

        member _.OpenFileForReadShim(filePath: string, ?useMemoryMappedFile: bool, ?shouldShadowCopy: bool) : Stream =
            let shouldShadowCopy = defaultArg shouldShadowCopy false
            let useMemoryMappedFile = defaultArg useMemoryMappedFile false
            this.OpenFileForReadShim(filePath, useMemoryMappedFile, shouldShadowCopy)

        member _.OpenFileForWriteShim(filePath: string, ?fileMode: FileMode, ?fileAccess: FileAccess, ?fileShare: FileShare) : Stream =
            let fileMode = defaultArg fileMode FileMode.OpenOrCreate
            let fileAccess = defaultArg fileAccess FileAccess.ReadWrite
            let fileShare = defaultArg fileShare FileShare.Delete ||| FileShare.ReadWrite
            this.OpenFileForWriteShim(filePath, fileMode, fileAccess, fileShare)

        member _.GetFullPathShim(fileName: string) = this.GetFullPathShim fileName

        member _.GetFullFilePathInDirectoryShim (dir: string) (fileName: string) =
            this.GetFullFilePathInDirectoryShim dir fileName

        member _.IsPathRootedShim(path: string) = this.IsPathRootedShim path
        member _.NormalizePathShim(path: string) = this.NormalizePathShim path
        member _.IsInvalidPathShim(path: string) = this.IsInvalidPathShim path
        member _.GetTempPathShim() = this.GetTempPathShim()
        member _.GetDirectoryNameShim(s: string) = this.GetDirectoryNameShim s
        member _.GetLastWriteTimeShim(fileName: string) = this.GetLastWriteTimeShim fileName
        member _.GetCreationTimeShim(path: string) = this.GetCreationTimeShim path
        member _.CopyShim(src: string, dest: string, overwrite: bool) = this.CopyShim(src, dest, overwrite)
        member _.FileExistsShim(fileName: string) = this.FileExistsShim fileName
        member _.FileDeleteShim(fileName: string) = this.FileDeleteShim fileName
        member _.DirectoryCreateShim(path: string) = this.DirectoryCreateShim path
        member _.DirectoryExistsShim(path: string) = this.DirectoryExistsShim path
        member _.DirectoryDeleteShim(path: string) = this.DirectoryDeleteShim path
        member _.EnumerateFilesShim(path: string, pattern: string) = this.EnumerateFilesShim(path, pattern)
        member _.EnumerateDirectoriesShim(path: string) = this.EnumerateDirectoriesShim path
        member _.IsStableFileHeuristic(fileName: string) = this.IsStableFileHeuristic fileName

        member _.ChangeExtensionShim(path: string, extension: string) =
            this.ChangeExtensionShim(path, extension)

[<AutoOpen>]
module public StreamExtensions =
    let utf8noBOM = UTF8Encoding(false, true) :> Encoding

    type Stream with

        member s.GetWriter(?encoding: Encoding) : TextWriter =
            let encoding = defaultArg encoding utf8noBOM
            new StreamWriter(s, encoding) :> TextWriter

        member s.WriteAllLines(contents: string seq, ?encoding: Encoding) =
            let encoding = defaultArg encoding utf8noBOM
            use writer = s.GetWriter(encoding)

            for l in contents do
                writer.WriteLine(l)

        member s.Write(data: 'a) : unit =
            use sw = s.GetWriter()
            sw.Write(data)

        member s.GetReader(codePage: int option, ?retryLocked: bool) =
            let retryLocked = defaultArg retryLocked false
            let retryDelayMilliseconds = 50
            let numRetries = 60

            let rec getSource retryNumber =
                try
                    // Use the .NET functionality to auto-detect the unicode encoding
                    match codePage with
                    | None -> new StreamReader(s, true)
                    | Some n -> new StreamReader(s, Encoding.GetEncoding(n))
                with
                // We can get here if the file is locked--like when VS is saving a file--we don't have direct
                // access to the HRESULT to see that this is EONOACCESS.
                | :? IOException as err when retryLocked && err.GetType() = typeof<IOException> ->
                    // This second check is to make sure the exception is exactly IOException and none of these for example:
                    //   DirectoryNotFoundException
                    //   EndOfStreamException
                    //   FileNotFoundException
                    //   FileLoadException
                    //   PathTooLongException
                    if retryNumber < numRetries then
                        Thread.Sleep retryDelayMilliseconds
                        getSource (retryNumber + 1)
                    else
                        reraise ()

            getSource 0

        member s.ReadBytes(start, len) =
            s.Seek(int64 start, SeekOrigin.Begin) |> ignore
            let buffer = Array.zeroCreate len
            let mutable n = 0

            while n < len do
                n <- n + s.Read(buffer, n, len - n)

            buffer

        member s.ReadAllBytes() =
            use reader = new BinaryReader(s)
            let count = (int s.Length)
            reader.ReadBytes(count)

        member s.ReadAllText(?encoding: Encoding) =
            let encoding = defaultArg encoding Encoding.UTF8
            use sr = new StreamReader(s, encoding, true)
            sr.ReadToEnd()

        member s.ReadLines(?encoding: Encoding) : string seq =
            let encoding = defaultArg encoding Encoding.UTF8

            seq {
                use sr = new StreamReader(s, encoding, true)

                while not <| sr.EndOfStream do
                    yield !!sr.ReadLine()
            }

        member s.ReadAllLines(?encoding: Encoding) : string array =
            let encoding = defaultArg encoding Encoding.UTF8
            s.ReadLines(encoding) |> Seq.toArray

        member s.WriteAllText(text: string) =
            use writer = new StreamWriter(s)
            writer.Write text

        /// If we are working with the view stream from mmf, we wrap it in RawByteMemory (which does zero copy, bu just using handle from the views stream).
        /// However, when we use any other stream (FileStream, MemoryStream, etc) - we just read everything from it and expose via ByteArrayMemory.
        member s.AsByteMemory() : ByteMemory =
            match s with
            | :? MemoryMappedStream as mmfs ->
                let length = mmfs.Length

                RawByteMemory(NativePtr.ofNativeInt (mmfs.ViewStream.SafeMemoryMappedViewHandle.DangerousGetHandle()), int length, mmfs)
                :> ByteMemory

            | _ ->
                let bytes = s.ReadAllBytes()

                let byteArrayMemory =
                    if bytes.Length = 0 then
                        ByteArrayMemory([||], 0, 0)
                    else
                        ByteArrayMemory(bytes, 0, bytes.Length)

                byteArrayMemory :> ByteMemory

[<AutoOpen>]
module public FileSystemAutoOpens =
    /// The global hook into the file system
    let mutable FileSystem: IFileSystem = DefaultFileSystem() :> IFileSystem

#endif //!FABLE_COMPILER

type ByteMemory with

    member x.AsReadOnly() = ReadOnlyByteMemory x

    static member Empty = ByteArrayMemory([||], 0, 0) :> ByteMemory

#if !FABLE_COMPILER
    static member FromMemoryMappedFile(mmf: MemoryMappedFile) =
        let accessor = mmf.CreateViewAccessor()
        RawByteMemory.FromUnsafePointer(accessor.SafeMemoryMappedViewHandle.DangerousGetHandle(), int accessor.Capacity, (mmf, accessor))

    static member FromUnsafePointer(addr, length, holder: obj) =
        RawByteMemory(NativePtr.ofNativeInt addr, length, holder) :> ByteMemory
#endif //!FABLE_COMPILER

    static member FromArray(bytes, offset, length) =
        ByteArrayMemory(bytes, offset, length) :> ByteMemory

    static member FromArray(bytes: byte array) =
        if bytes.Length = 0 then
            ByteMemory.Empty
        else
            ByteArrayMemory.FromArray(bytes, 0, bytes.Length)

type internal ByteStream =
    {
        bytes: ReadOnlyByteMemory
        mutable pos: int
        max: int
    }

    member b.IsEOF = (b.pos >= b.max)

    member b.ReadByte() =
        if b.pos >= b.max then
            failwith "end of stream"

        let res = b.bytes[b.pos]
        b.pos <- b.pos + 1
        res

    member b.ReadUtf8String n =
        let res = b.bytes.ReadUtf8String(b.pos, n)
        b.pos <- b.pos + n
        res

    static member FromBytes(b: ReadOnlyByteMemory, start, length) =
        if start < 0 || (start + length) > b.Length then
            failwith "FromBytes"

        {
            bytes = b
            pos = start
            max = start + length
        }

    member b.ReadBytes n =
        if b.pos + n > b.max then
            failwith "ReadBytes: end of stream"

        let res = b.bytes.Slice(b.pos, n)
        b.pos <- b.pos + n
        res

    member b.Position = b.pos

type internal ByteBuffer =
    {
        useArrayPool: bool
        mutable isDisposed: bool
        mutable bbArray: byte[]
        mutable bbCurrent: int
    }

    member inline private buf.CheckDisposed() =
        if buf.isDisposed then
            raise (ObjectDisposedException(nameof ByteBuffer))

    member private buf.Ensure newSize =
        let oldBufSize = buf.bbArray.Length

        if newSize > oldBufSize then
            let old = buf.bbArray

            buf.bbArray <-
#if !FABLE_COMPILER
                if buf.useArrayPool then
                    ArrayPool.Shared.Rent(max newSize (oldBufSize * 2))
                else
#endif
                    Bytes.zeroCreate (max newSize (oldBufSize * 2))

            Bytes.blit old 0 buf.bbArray 0 buf.bbCurrent

#if !FABLE_COMPILER
            if buf.useArrayPool then
                ArrayPool.Shared.Return old
#endif

#if FABLE_COMPILER
    member buf.Close () = Array.sub buf.bbArray 0 buf.bbCurrent
#else
    member buf.AsMemory() =
        buf.CheckDisposed()
        ReadOnlyMemory(buf.bbArray, 0, buf.bbCurrent)
#endif

    member buf.EmitIntAsByte(i: int) =
        buf.CheckDisposed()
        let newSize = buf.bbCurrent + 1
        buf.Ensure newSize
        buf.bbArray[buf.bbCurrent] <- byte i
        buf.bbCurrent <- newSize

    member buf.EmitByte(b: byte) =
        buf.CheckDisposed()
        buf.EmitIntAsByte(int b)

    member buf.EmitIntsAsBytes(arr: int[]) =
        buf.CheckDisposed()
        let n = arr.Length
        let newSize = buf.bbCurrent + n
        buf.Ensure newSize
        let bbArr = buf.bbArray
        let bbBase = buf.bbCurrent

        for i = 0 to n - 1 do
            bbArr[bbBase + i] <- byte arr[i]

        buf.bbCurrent <- newSize

    member bb.FixupInt32 pos value =
        bb.CheckDisposed()
        bb.bbArray[pos] <- (Bytes.b0 value |> byte)
        bb.bbArray[pos + 1] <- (Bytes.b1 value |> byte)
        bb.bbArray[pos + 2] <- (Bytes.b2 value |> byte)
        bb.bbArray[pos + 3] <- (Bytes.b3 value |> byte)

    member buf.EmitInt32 n =
        buf.CheckDisposed()
        let newSize = buf.bbCurrent + 4
        buf.Ensure newSize
        buf.FixupInt32 buf.bbCurrent n
        buf.bbCurrent <- newSize

    member buf.EmitBytes(i: byte[]) =
        buf.CheckDisposed()
        let n = i.Length
        let newSize = buf.bbCurrent + n
        buf.Ensure newSize
        Bytes.blit i 0 buf.bbArray buf.bbCurrent n
        buf.bbCurrent <- newSize

#if !FABLE_COMPILER
    member buf.EmitMemory(i: ReadOnlyMemory<byte>) =
        buf.CheckDisposed()
        let n = i.Length
        let newSize = buf.bbCurrent + n
        buf.Ensure newSize
        i.CopyTo(Memory(buf.bbArray, buf.bbCurrent, n))
        buf.bbCurrent <- newSize

    member buf.EmitByteMemory(i: ReadOnlyByteMemory) =
        buf.CheckDisposed()
        let n = i.Length
        let newSize = buf.bbCurrent + n
        buf.Ensure newSize
        i.Copy(0, buf.bbArray, buf.bbCurrent, n)
        buf.bbCurrent <- newSize
#endif //!FABLE_COMPILER

    member buf.EmitInt32AsUInt16 n =
        buf.CheckDisposed()
        let newSize = buf.bbCurrent + 2
        buf.Ensure newSize
        buf.bbArray[buf.bbCurrent] <- (Bytes.b0 n |> byte)
        buf.bbArray[buf.bbCurrent + 1] <- (Bytes.b1 n |> byte)
        buf.bbCurrent <- newSize

    member buf.EmitBoolAsByte(b: bool) =
        buf.CheckDisposed()
        buf.EmitIntAsByte(if b then 1 else 0)

    member buf.EmitUInt16(x: uint16) =
        buf.CheckDisposed()
        buf.EmitInt32AsUInt16(int32 x)

    member buf.EmitInt64 x =
        buf.CheckDisposed()
        buf.EmitInt32(Bytes.dWw0 x)
        buf.EmitInt32(Bytes.dWw1 x)

    member buf.Position =
        buf.CheckDisposed()
        buf.bbCurrent

    static member Create(capacity, useArrayPool) =
        let useArrayPool = defaultArg useArrayPool false

        {
            useArrayPool = useArrayPool
            isDisposed = false
#if FABLE_COMPILER
            bbArray = Bytes.zeroCreate capacity
#else
            bbArray =
                if useArrayPool then
                    ArrayPool.Shared.Rent capacity
                else
                    Bytes.zeroCreate capacity
#endif
            bbCurrent = 0
        }

    interface IDisposable with

        member this.Dispose() =
            if not this.isDisposed then
                this.isDisposed <- true

#if !FABLE_COMPILER
                if this.useArrayPool then
                    ArrayPool.Shared.Return this.bbArray
#endif

#if !FABLE_COMPILER

[<Sealed>]
type ByteStorage(getByteMemory: unit -> ReadOnlyByteMemory) =

    let mutable cached = Unchecked.defaultof<WeakReference<ByteMemory>>

    let getAndCache () =
        let byteMemory = getByteMemory ()
        cached <- WeakReference<ByteMemory>(byteMemory.Underlying)
        byteMemory

    member _.GetByteMemory() =
        match box cached with
        | null -> getAndCache ()
        | _ ->
            match cached.TryGetTarget() with
            | true, byteMemory -> byteMemory.AsReadOnly()
            | _ -> getAndCache ()

    static member FromByteArray(bytes: byte[]) =
        ByteStorage.FromByteMemory(ByteMemory.FromArray(bytes).AsReadOnly())

    static member FromByteMemory(bytes: ReadOnlyByteMemory) = ByteStorage(fun () -> bytes)

    static member FromByteMemoryAndCopy(bytes: ReadOnlyByteMemory, useBackingMemoryMappedFile: bool) =
        if useBackingMemoryMappedFile then
            match MemoryMappedFile.TryFromByteMemory(bytes) with
            | Some mmf -> ByteStorage(fun () -> ByteMemory.FromMemoryMappedFile(mmf).AsReadOnly())
            | _ ->
                let copiedBytes = ByteMemory.FromArray(bytes.ToArray()).AsReadOnly()
                ByteStorage.FromByteMemory(copiedBytes)
        else
            let copiedBytes = ByteMemory.FromArray(bytes.ToArray()).AsReadOnly()
            ByteStorage.FromByteMemory(copiedBytes)

    static member FromMemoryAndCopy(bytes: ReadOnlyMemory<byte>, useBackingMemoryMappedFile: bool) =
        if useBackingMemoryMappedFile then
            match MemoryMappedFile.TryFromMemory(bytes) with
            | Some mmf -> ByteStorage(fun () -> ByteMemory.FromMemoryMappedFile(mmf).AsReadOnly())
            | _ ->
                let copiedBytes = ByteMemory.FromArray(bytes.ToArray()).AsReadOnly()
                ByteStorage.FromByteMemory(copiedBytes)
        else
            let copiedBytes = ByteMemory.FromArray(bytes.ToArray()).AsReadOnly()
            ByteStorage.FromByteMemory(copiedBytes)

    static member FromByteArrayAndCopy(bytes: byte[], useBackingMemoryMappedFile: bool) =
        ByteStorage.FromByteMemoryAndCopy(ByteMemory.FromArray(bytes).AsReadOnly(), useBackingMemoryMappedFile)

#endif //!FABLE_COMPILER
